package bufmgr;

import global.Convert;
import global.GlobalConst;
import global.PageId;
import global.SystemDefs;

import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;

import chainexception.ChainException;
import HashTable.MyHashTable;
import diskmgr.DiskMgrException;
import diskmgr.FileIOException;

import diskmgr.InvalidPageNumberException;
import diskmgr.InvalidRunSizeException;
import diskmgr.OutOfSpaceException;
import diskmgr.Page;

public class BufMgr implements GlobalConst {
	private byte[][] pool;
	private Descriptor[] desArray;
	private MyHashTable<Integer> hash;
	private Queue<Integer> emptyFrams = new LinkedBlockingQueue<Integer>();

	// private DB myData = new DB();
	// private final String dataBaseName = "myData";

	// open the data base
	/**
	 * create the bufMgr Object allocate pages for the buffer pool in main
	 * memory and make the buffer manager aware the replace policy is specified
	 * by replacerArg
	 * 
	 * @param numbufs
	 *            number of buffers in pool
	 * @param replacerArg
	 *            name of replace arg policy
	 */
	public BufMgr(int numbufs, String replacerArg) throws OutOfSpaceException,
			InvalidRunSizeException, InvalidPageNumberException,
			FileIOException, DiskMgrException, IOException {
		pool = new byte[numbufs][MAX_SPACE];
		desArray = new Descriptor[numbufs];
		hash = new MyHashTable<Integer>(numbufs);
		init(numbufs);
	}

	/**
	 * Pin a page. First check if this page is already in the buffer pool. If it
	 * is, increment the pin_count and return a pointer to this page. If the
	 * pin_count was 0 before the call, the page was a replacement candidate,
	 * but is no longer a candidate. If the page is not in the pool, choose a
	 * frame (from the set of replacement candidates) to hold this page, read
	 * the page (using the appropriate method from {\em diskmgr} package) and
	 * pin it. Also, must write out the old page in chosen frame if it is dirty
	 * before reading new page. (You can assume that emptyPage==false for this
	 * assignment.)
	 * 
	 * @param Page_Id_in_a_DB
	 *            page number in the minibase.
	 * @param page
	 *            the pointer pointer to the page.
	 * @param emptyPage
	 *            true (empty page); false (non-empty page)
	 * @throws IOException
	 * @throws FileIOException
	 * @throws InvalidPageNumberException
	 * @throws FileIOException
	 * @throws InvalidPageNumberException
	 * @throws DiskMgrException
	 * @throws PageUnpinnedException
	 * @throws BufferPoolExceededException
	 */
	public void pinPage(PageId pin_pgid, Page page, boolean emptyPage)
			throws IOException, InvalidPageNumberException, FileIOException,
			DiskMgrException, PageUnpinnedException,
			BufferPoolExceededException {
		PageId writablePageId = new PageId(INVALID_PAGE);
		boolean isWritable = false;
		int frameNumber = hash.get(pin_pgid.pid);

		if (frameNumber < 0) {
			try {
				// check if it's still empty frame to pin page
				frameNumber = emptyFrams.poll();
			} catch (Exception e) {
				throw new BufferPoolExceededException(e, "Queue is Empty");
			}
			Descriptor victimPage = desArray[frameNumber];
			// all descriptor's ID initially is -1 so check before
			if ((victimPage.getPageNumber().pid != INVALID_PAGE)
					&& (victimPage.isDirtyBit())) {
				// change the boolean variable to write it later
				isWritable = true;
				writablePageId.pid = victimPage.getPageNumber().pid;
			}
			// remove the old page from the hash
			hash.remove(victimPage.getPageNumber().pid);
			// set the Descriptor according to the new page
			desArray[frameNumber].setPinCount(1);
			desArray[frameNumber].getPageNumber().pid = pin_pgid.pid;
			desArray[frameNumber].setDirtyBit(false);
			if (!hash.put(pin_pgid.pid, frameNumber)) {
				// put the new page in hash
				throw new PageUnpinnedException(null,
						"BufMgr.pinPage(): HashOperationException");
			}
			Page writablePage = new Page(pool[frameNumber]);
			if (isWritable) {
				SystemDefs.JavabaseDB.write_page(writablePageId, writablePage);
			}
			// if (!emptyPage) {
			try {
				// trying to read page
				Page p = new Page(page.getpage());
				pool[frameNumber] = p.getpage();
				SystemDefs.JavabaseDB.read_page(pin_pgid, writablePage);
				pool[frameNumber] = writablePage.getpage();
			} catch (Exception e) {
			
				final Descriptor frameDesc = desArray[frameNumber];
				final PageId pageId = frameDesc.getPageNumber();
				if (!hash.remove(pageId.pid)) {
					throw new PageUnpinnedException(e,
							"BufMgr.pinPage(): HashOperationException");
				}
				frameDesc.setDirtyBit(false);
				Iterator<Integer> itQ = emptyFrams.iterator();

				while (itQ.hasNext()) {
					// removing the frame from queue
					int curr = itQ.next();
					if (frameNumber == curr) {
						itQ.remove();
						break;

					}
				}
				// if (!replacer.unpin(frameNumber)) {
				// throw new ReplacerException(e,
				// "BufMgr.pinPage(): ReplacerException");
				// }
				throw new PageUnpinnedException(e,
						"BufMgr.pinPage(): PageNotReadException");
			}
			//set page data to the data in buffer pool
			page.setpage(pool[frameNumber]);
			// }

		} else {
			// the simple case we found it on the hash
			page.setpage(pool[frameNumber]);
			Descriptor frameDesc = desArray[frameNumber];
			frameDesc.setPinCount(frameDesc.getPinCount() + 1);
			if (frameDesc.getPinCount() == 1) {
				Iterator<Integer> itQ = emptyFrams.iterator();

				while (itQ.hasNext()) {
					int curr = itQ.next();
					if (frameNumber == curr) {
						itQ.remove();
						break;

					}
				}
			}
		}
		// System.out.println("data writing in pool");

	};

	/**
	 * Unpin a page specified by a pageId. This method should be called with
	 * dirty==true if the client has modified the page. If so, this call should
	 * set the dirty bit for this frame. Further, if pin_count>0, this method
	 * should decrement it. If pin_count=0 before this call, throw an exception
	 * to report error. (For testing purposes, we ask you to throw an exception
	 * named PageUnpinnedException in case of error.)
	 * 
	 * @param globalPageId_in_a_DB
	 *            page number in the minibase.
	 * @param dirty
	 *            the dirty bit of the frame
	 * @throws PageUnpinnedException
	 * @throws IOException
	 * @throws FileIOException
	 * @throws InvalidPageNumberException
	 * @throws HashEntryNotFoundException
	 */
	public void unpinPage(PageId PageId_in_a_DB, boolean dirty)
			throws PageUnpinnedException, InvalidPageNumberException,
			FileIOException, IOException, HashEntryNotFoundException {
		// if pin count > 0 then decrement it and set the dirty bit if any
		// changes happened
		// else throw exception "page not found || unPin page with pin count =0

		int frameNumber = hash.get(PageId_in_a_DB.pid);
		if (frameNumber != -1) {
			Descriptor requiredPage = desArray[frameNumber];
			int pinsNumber = requiredPage.getPinCount();
			// user finish using the page
			if (pinsNumber > 0) {
				if (dirty) {
					requiredPage.setDirtyBit(true);
				}
				if (pinsNumber == 1) {
					//no one using it
					// if(emptyFrams.size()<NUMBUF){
					emptyFrams.add(frameNumber);
					// }
				}
				requiredPage.setPinCount(pinsNumber - 1);
				desArray[frameNumber] = requiredPage;
			} else { // unpin for not using page

				throw new PageUnpinnedException(null, "No one Using it");
			}
		} else {
			// not found in the Linked List
			throw new HashEntryNotFoundException(null, "Not found in hash");
		}

	};

	/**
	 * Allocate new pages. Call DB object to allocate a run of new pages and
	 * find a frame in the buffer pool for the first page and pin it. (This call
	 * allows a client of the Buffer Manager to allocate pages on disk.) If
	 * buffer is full, i.e., you canÂ’t find a frame for the first page, ask
	 * DB to deallocate all these pages, and return null.
	 * 
	 * @param firstpage
	 *            the address of the first page.
	 * @param howmany
	 *            total number of allocated new pages.
	 * 
	 * @return the first page id of the new pages. null, if error.
	 * @throws IOException
	 * @throws DiskMgrException
	 * @throws FileIOException
	 * @throws InvalidPageNumberException
	 * @throws InvalidRunSizeException
	 * @throws OutOfSpaceException
	 */
	public PageId newPage(Page firstpage, int howmany)
			throws OutOfSpaceException, InvalidRunSizeException,
			InvalidPageNumberException, FileIOException, DiskMgrException,
			IOException {
		PageId p = new PageId();
		try {
//allocating pages on the desk
			SystemDefs.JavabaseDB.allocate_page(p, howmany);

		} catch (Exception e) {
			// throw new DiskMgrException(e, "DB.java: pinPage() failed");
		}
		// System.out.println("pid = " + p.pid);
		try {
			pinPage(p, firstpage, true);
			return p;
		} catch (Exception e) {
			try {
				SystemDefs.JavabaseDB.deallocate_page(p, howmany);
			} catch (Exception e2) {
				throw new DiskMgrException(e, "DB.java: pinPage() failed");

			}
		}
		// SystemDefs.JavabaseDB.closeDB();
		return null;

	};

	/**
	 * Used to flush a particular page of the buffer pool to disk. This method
	 * calls the write_page method of the diskmgr package.
	 * 
	 * @param pageid
	 *            the page number in the database.
	 * @throws PageUnpinnedException
	 * @throws PageUnpinnedException
	 * @throws IOException
	 * @throws FileIOException
	 * @throws InvalidPageNumberException
	 * @throws HashEntryNotFoundException
	 */
	public void flushPage(PageId pageid) throws PageUnpinnedException,
			InvalidPageNumberException, FileIOException, IOException,
			HashEntryNotFoundException {
		int frameNumber = hash.get(pageid.pid);

		if (frameNumber != -1) {
			Descriptor requiredPage = desArray[frameNumber];
			int pinsNumber = requiredPage.getPinCount();
			// user finish using the page
			if (pinsNumber > 0) {
				// which page should i write ?
				SystemDefs.JavabaseDB.write_page(requiredPage.getPageNumber(),
						new Page(pool[frameNumber]));
			} else { // unpin for not using page
				throw new PageUnpinnedException(null, "No one Using it");
			}
		} else {
			// not found in the Linked List
			throw new HashEntryNotFoundException(null, "Not found in hash");

		}
	};

	/**
	 * This method should be called to delete a page that is on disk. This
	 * routine must call the method in diskmgr package to deallocate the page.
	 * 
	 * @param globalPageId
	 *            the page number in the data base.
	 * @throws IOException
	 * @throws DiskMgrException
	 * @throws FileIOException
	 * @throws InvalidPageNumberException
	 * @throws InvalidRunSizeException
	 * @throws PageUnpinnedException
	 * @throws PagePinnedException
	 */
	public void freePage(PageId globalPageId) throws InvalidRunSizeException,
			InvalidPageNumberException, FileIOException, DiskMgrException,
			IOException, PageUnpinnedException, PagePinnedException {

		int frameNumber = hash.get(globalPageId.pid);
		if (frameNumber < 0) {
			// not in the hash but on the disk
			try {
				
				SystemDefs.JavabaseDB.deallocate_page(globalPageId);
			} catch (Exception e) {
				// if not in the disk throw error
				throw new DiskMgrException(null, "DB.java: pinPage() failed");

			}
			return;
		} else {
			Descriptor descriptor = desArray[frameNumber];

			if (descriptor.getPinCount() > 1)
				throw new PagePinnedException(null, "More than one user ");
			else {
				PageId pageId = descriptor.getPageNumber();
				hash.remove(pageId.pid);
				if (emptyFrams.size() < desArray.length) {
					emptyFrams.add(frameNumber);
				}
				// pageId.pid = INVALID_PAGE;
				descriptor.setDirtyBit(false);
				try {
					SystemDefs.JavabaseDB.deallocate_page(descriptor
							.getPageNumber());
				} catch (Exception e) {
					throw new DiskMgrException(e, "DB.java: pinPage() failed");
				}
			}

		}
	};

	/**
	 * counts and return the number of buffers unused that's on the queue
	 * 
	 * @return number of unpinned buffers
	 */
	public int getNumUnpinnedBuffers() {
		// TODO Auto-generated method stub
		return emptyFrams.size();
	}

	/**
	 * 
	 * @throws PageUnpinnedException
	 * @throws InvalidPageNumberException
	 * @throws FileIOException
	 * @throws IOException
	 * @throws HashEntryNotFoundException
	 */

	public void flushAllPages() throws PageUnpinnedException,
			InvalidPageNumberException, FileIOException, IOException,
			HashEntryNotFoundException {

		for (int i = 0; i < desArray.length; i++) {
			if (desArray[i] != null) {
				if (desArray[i].getPinCount() != 0) {
					flushPage(desArray[i].getPageNumber());
				}
			}
		}
	}

	private void init(int numbufs) {

		for (int i = 0; i < numbufs; i++) {
			emptyFrams.add(i);
		}

		for (int i = 0; i < desArray.length; i++) {
			PageId initial = new PageId(INVALID_PAGE);
			Descriptor de = new Descriptor(initial);
			desArray[i] = de;
		}
	}

	public static void main(String[] args) {

	}
}
